package com.googlecode.beanfiles;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.googlecode.beanfiles.translators.ConvertUtilsTranslator;
import com.googlecode.beanfiles.translators.NestedConvertUtilsTranslator;
import com.googlecode.beanfiles.translators.NestedPropertyTranslator;
import com.googlecode.beanfiles.translators.PatternTranslator;
import com.googlecode.beanfiles.translators.PropertyTranslator;

public class DefaultTranslatorMapFactoryImpl implements TranslatorsMapFactory {

    protected static Log log() {
        return LogFactory.getLog(DefaultTranslatorMapFactoryImpl.class);
    }

    public DefaultTranslatorMapFactoryImpl() {}

    private boolean ignoreInvalidHeadings;
    private boolean ignoreMissingHeadings;
    private Map<String, ? extends PropertyTranslator> translators;
    private Map<String, String> headingMap;

    /**
     * Constructs a translator map factory implementation that will created ordered translator maps based on the provided heading map along
     * with the provided translator map. When a translator is found in the provided translators map, this translator is used, otherwise a
     * default is provided.
     * 
     * @param translators
     * @param headingMap
     */
    public DefaultTranslatorMapFactoryImpl(Map<String, ? extends PropertyTranslator> translators, Map<String, String> headingMap) {
        this.translators = translators;
        this.headingMap = headingMap;
    }

    public void setIgnoreInvalidHeadings(boolean ignoreInvalidHeadings) {
        this.ignoreInvalidHeadings = ignoreInvalidHeadings;
    }

    public void setIgnoreMissingHeadings(boolean ignoreMissingHeadings) {
        this.ignoreMissingHeadings = ignoreMissingHeadings;
    }

    public DefaultTranslatorMapFactoryImpl(Map<String, String> headingMap) {
        this.headingMap = headingMap;
    }

    public Map<String, ? extends PropertyTranslator> getTranslators() {
        return translators;
    }

    public void setCustomTranslators(Map<String, ? extends PropertyTranslator> translators) {
        this.translators = translators;
    }

    public Map<String, String> getHeadingMap() {
        return headingMap;
    }

    public void setHeadingMap(Map<String, String> headingMap) {
        this.headingMap = headingMap;
    }

    public Map<String, String> getOriginalPatterns() {
        return originalPatterns;
    }

    public void setOriginalPatterns(Map<String, String> originalPatterns) {
        this.originalPatterns = originalPatterns;
    }

    /**
     * Build an ordered map based on the heading values provided by the input, using 1) translators found within the translators map or
     * 2) default implementations when no translators are provided.
     * 
     * @param <I>
     * @param <T>
     * @param input
     * @param targetFactory
     * @return
     */
    public <T> Map<String, ? extends PropertyTranslator> buildMap(Collection<Object> input, TargetFactory<T> targetFactory) {
        if (!(input instanceof Collection)) {
            throw new TranslationException("Currently, Collection input support only.");
        }
        return buildTranslatorsMap(getPropertyNames(input, targetFactory), targetFactory.createTarget().getClass());
    }

    private <T> List<String> getPropertyNames(Collection<Object> input, TargetFactory<T> targetFactory) {
        List<String> propertyNames = new ArrayList<String>();
        Collection<String> propertyKeys = null;
        List<String> invalidHeadings = new ArrayList<String>();

        if (headingMap == null) {
            propertyKeys = TranslatorMapUtils.buildNestedPropertiesList(targetFactory.createTarget().getClass());
        } else {
            propertyKeys = headingMap.keySet();
        }

        out: for (Object headingO : input) {
            String heading = String.valueOf(headingO).trim();
            for (String mapKey : propertyKeys) {
                String propertyName = null;
                if (headingMap == null) {
                    propertyName = mapKey;
                } else {
                    propertyName = headingMap.get(mapKey);
                }

                if (mapKey.endsWith("*")) {
                    // add pattern matching
                    if (heading.startsWith(mapKey.substring(0, mapKey.indexOf("*")))) {
                        addOriginalPatternMapping(propertyName + "*", mapKey);
                        propertyNames.add(propertyName + "*" + heading.substring(mapKey.length() - 1).trim());
                        continue out;
                    }
                } else if (mapKey.equals(heading)) {
                    // add plain mapping
                    propertyNames.add(propertyName);
                    continue out;
                }
            }
            invalidHeadings.add(heading);
        }

        validateHeadings(propertyNames, invalidHeadings);
        return propertyNames;
    }

    private void validateHeadings(List<String> propertyNames, List<String> invalidHeadings) {
        if (!ignoreMissingHeadings && !ignoreInvalidHeadings) {
            HeadingException exception = new HeadingException("Invalid header");
            boolean willThrow = false;
            if (!invalidHeadings.isEmpty()) {
                exception.setInvalidHeadings(invalidHeadings);
                willThrow = true;
            }
            List<String> missingHeadings = getMissingHeadings(propertyNames, headingMap);
            if (missingHeadings != null && !missingHeadings.isEmpty()) {
                exception.setMissingHeadings(missingHeadings);
                willThrow = true;
            }
            if (willThrow) {
                throw exception;
            }
        } else if (!ignoreMissingHeadings) {
            List<String> missingHeadings = getMissingHeadings(propertyNames, headingMap);
            if (missingHeadings != null && !missingHeadings.isEmpty()) {
                throw new MissingHeadingException("Missing heading" + ((missingHeadings.size() == 1) ? "" : "s"), missingHeadings);
            }
        } else if (!ignoreInvalidHeadings && !invalidHeadings.isEmpty()) {
            throw new InvalidHeadingException("Invalid heading" + ((invalidHeadings.size() == 1) ? "" : "s"), invalidHeadings);
        }
    }

    private List<String> getMissingHeadings(Collection<String> propertyNames, Map<String, String> headingMap) {
        if (headingMap == null) {
            return null;
        }
        List<String> missingHeadings = new ArrayList<String>();
        for (String property : headingMap.values()) {
            if (!propertyNames.contains(property) && !isWildCardMapping(property)) {
                missingHeadings.add(property);
            }
        }
        return missingHeadings;
    }

    private boolean isWildCardMapping(String property) {
        return originalPatterns.containsKey(property + "*");
    }

    /**
     * Builds a map of PropertyTranslators based on a properties list
     * 
     * Special care is given to property names containing '.' characters. In such cases, a NestedConvertUtilsTranslator is created to populate
     * a nested bean.
     * 
     * Special care is also given to property names containing a '*' character. Note that only simple xxx* pattern matching is supported.
     * Note that there is currently no support for pattern matching of nested properties. If this is attempted, a TranslationException is
     * raised.
     * 
     * @param clazz
     * @param propertiesMap
     * @return
     */
    private <T> Map<String, ? extends PropertyTranslator> buildTranslatorsMap(List<String> properties, Class<T> clazz) {

        Map<String, PropertyTranslator> map = new HashMap<String, PropertyTranslator>();
        int index = 0;
        Map<String, Map<String, List<Integer>>> rootPropertiesMap = new HashMap<String, Map<String, List<Integer>>>();
        Map<String, PatternTranslator> patternTranslators = new HashMap<String, PatternTranslator>();

        for (String absolutePropertyName : properties) {
            if (absolutePropertyName.indexOf('.') != -1 && absolutePropertyName.indexOf("*") != -1) {
                throw new TranslationException("nested pattern matching is not supported");
            }
            if (absolutePropertyName.indexOf('.') != -1) {
                // nested properties
                String propertyName = absolutePropertyName.substring(0, absolutePropertyName.indexOf('.'));
                String relativePropertyName =
                        absolutePropertyName.substring(absolutePropertyName.indexOf('.') + 1, absolutePropertyName.length());

                addNestedPropertyIndex(index++, rootPropertiesMap, propertyName, relativePropertyName);
            } else if (absolutePropertyName.contains("*")) {
                // pattern match property
                addPatternPropertyMatch(index++, patternTranslators, absolutePropertyName);
            } else {
                // plain property
                map.put(absolutePropertyName, getPropertyTranslator(absolutePropertyName, index++));
            }
        }

        // add nested property translators
        for (String propertyName : rootPropertiesMap.keySet()) {
            map.put(propertyName, getNestedTranslator(rootPropertiesMap, propertyName));
        }

        // add pattern match translators
        for (String propertyName : patternTranslators.keySet()) {
            map.put(propertyName, patternTranslators.get(propertyName));
        }

        return map;
    }

    private NestedPropertyTranslator getNestedTranslator(Map<String, Map<String, List<Integer>>> rootPropertiesMap, String propertyName) {
        NestedPropertyTranslator translator = (NestedPropertyTranslator) getTranslator(propertyName);
        if (translator != null) {
            translator = (NestedPropertyTranslator) translator.clone();
            translator.setPropertyIndexesMap(rootPropertiesMap.get(propertyName));
            return translator;
        } else {
            return new NestedConvertUtilsTranslator(propertyName, rootPropertiesMap.get(propertyName));
        }
    }

    private void addNestedPropertyIndex(int index, Map<String, Map<String, List<Integer>>> rootPropertiesMap, String propertyName,
            String relativePropertyName) {
        Map<String, List<Integer>> nestedPropertiesMap = rootPropertiesMap.get(propertyName);
        if (nestedPropertiesMap == null) {
            nestedPropertiesMap = new HashMap<String, List<Integer>>();
            rootPropertiesMap.put(propertyName, nestedPropertiesMap);
        }
        List<Integer> nestedPropertyIndex = nestedPropertiesMap.get(relativePropertyName);
        if (nestedPropertyIndex == null) {
            nestedPropertyIndex = new ArrayList<Integer>();
            nestedPropertiesMap.put(relativePropertyName, nestedPropertyIndex);
        }
        nestedPropertyIndex.add(index);
    }

    private void addPatternPropertyMatch(int index, Map<String, PatternTranslator> patternTranslators, String propertyName) {

        String[] patternSplit = propertyName.split("\\*");
        PatternTranslator patternTranslator = patternTranslators.get(patternSplit[0]);

        if (patternTranslator == null) {
            patternTranslator = (PatternTranslator) getTranslator(originalPatterns.get(patternSplit[0] + "*"));
            if (patternTranslator == null) {
                patternTranslator = new PatternTranslator();
            } else {
                patternTranslator = (PatternTranslator) patternTranslator.clone();
            }
            patternTranslators.put(patternSplit[0], patternTranslator);
        }
        patternTranslator.addMatch(patternSplit[1], index);

    }

    private Map<String, String> originalPatterns = new HashMap<String, String>();

    private void addOriginalPatternMapping(String originalPattern, String refinedPattern) {
        originalPatterns.put(originalPattern, refinedPattern);
    }

    private PropertyTranslator getPropertyTranslator(String propertyName, int index) {
        PropertyTranslator translator = getTranslator(propertyName);
        if (translator == null) {
            return new ConvertUtilsTranslator(propertyName, index);
        } else {
            translator = (PropertyTranslator) translator.clone();
            translator.addIndex(index);
            return translator;
        }
    }

    private PropertyTranslator getTranslator(String propertyName) {
        if (translators == null) {
            return null;
        } else {
            return translators.get(propertyName);
        }
    }
}
